use std::collections::{BTreeMap, HashMap};
use std::path::PathBuf;

use config::{Config, ValueKind};
use log::info;

use crate::tiling::tiles::{Connectors, Tile, TileType, Unpackable};
use crate::Error;

#[cfg(test)]
mod tests;

#[cfg_attr(test, derive(Clone, Debug, PartialEq))]
pub(crate) struct TileList {
    pub(crate) tiles: Vec<Tile>,
    pub(crate) horizontal_connector_names: Vec<String>,
    pub(crate) vertical_connector_names: Vec<String>,
    pub(crate) tile_type: TileType,
}

impl TileList {
    pub(crate) fn new(config: Config) -> Result<Self, Error> {
        let (
            mut horizontal_connector_names,
            mut vertical_connector_names,
            mut horizontal_connector_map,
            mut vertical_connector_map,
            mut current_horizontal_connector_id,
            mut current_vertical_connector_id,
            mut tiles,
            mut tile_id,
        ) = Self::initialize_data_structures();

        let tile_directory = config.get_string("tile_directory")?;

        let tile_type = if let Ok(config_tiles) = config.get_array("tiles") {
            // config contains tiles, use them.
            // Extract the file path of the first tile to determine the tile constructor.
            let first_tile_path = match config_tiles.first() {
                None => {
                    return Err(Error::ValueError(
                        "the list of tiles is empty, it must contain at least one element"
                            .to_string(),
                    ));
                },
                Some(entry) => {
                    let entry = entry.kind.unpack_table()?;
                    if let Some(path) = entry.get("path") {
                        path.unpack_string()?
                    } else {
                        return Err(Error::ValueError(format!(
                            "the first tile is missing the mandatory “path” key:\n{entry:#?}"
                        )));
                    }
                },
            };

            let tile_type = TileType::determine_by_file_path(
                &PathBuf::from(tile_directory.clone()).join(first_tile_path),
            )?;
            let tile_constructor = Tile::determine_constructor_by_tile_type(&tile_type);

            // Iterate over all tile entries.
            for entry in config_tiles {
                let mut entry = entry.kind.unpack_table()?;

                let path_value = entry.get("path").ok_or(Error::ValueError(format!(
                    "this tile is missing the mandatory “path” key:\n{entry:#?}"
                )))?;
                let path = path_value.unpack_string()?;

                let name = if let Some(name_value) = entry.get("name") {
                    name_value.unpack_string()?
                } else {
                    entry.insert("name".to_string(), path_value);
                    path.clone()
                };

                if tiles.iter().any(|tile: &Tile| tile.name == name) {
                    return Err(Error::ValueError(format!(
                        "there are multiple tiles with the name “{name}”"
                    )));
                }

                let connectors = entry
                    .get("connectors")
                    .ok_or(Error::ValueError(format!(
                        "this tile is missing the mandatory “connectors” key:\n{entry:#?}"
                    )))?
                    .unpack_table()?;

                let weight = if let Some(weight_value) = entry.get("weight") {
                    match weight_value.unpack_numeric_to_f64() {
                        Ok(weight) => weight,
                        Err(error) => {
                            return Err(Error::ValueError(format!(
                                "the “weight” of the tile with the name “{name}” has the \
                                 following error: {error}"
                            )));
                        },
                    }
                } else {
                    1.
                };

                let tile = tile_constructor(
                    tile_id,
                    name.clone(),
                    PathBuf::from(tile_directory.clone()).join(path.clone()),
                    Connectors {
                        north: process_connector(
                            &connectors,
                            "north",
                            &entry,
                            &name,
                            &mut vertical_connector_map,
                            &mut vertical_connector_names,
                            &mut current_vertical_connector_id,
                        )?,
                        east: process_connector(
                            &connectors,
                            "east",
                            &entry,
                            &name,
                            &mut horizontal_connector_map,
                            &mut horizontal_connector_names,
                            &mut current_horizontal_connector_id,
                        )?,
                        south: process_connector(
                            &connectors,
                            "south",
                            &entry,
                            &name,
                            &mut vertical_connector_map,
                            &mut vertical_connector_names,
                            &mut current_vertical_connector_id,
                        )?,
                        west: process_connector(
                            &connectors,
                            "west",
                            &entry,
                            &name,
                            &mut horizontal_connector_map,
                            &mut horizontal_connector_names,
                            &mut current_horizontal_connector_id,
                        )?,
                    },
                    weight,
                )?;
                tiles.push(tile);

                // Bump tile_id for the next iteration
                tile_id += 1;
            }

            tile_type
        } else {
            // config contains no tiles.

            // This is only used for virtually unreachable cases in globbing and has no
            // deeper meaning.
            let empty_path = PathBuf::new();

            // It is not feasible to try to iterate over config::FileFormat, so they are
            // hardcoded.
            const ACCEPTED_CONFIG_FILE_FORMATS: [&'static str; 7] =
                ["ini", "json", "json5", "ron", "toml", "yaml", "yml"];

            let (last, elements) = ACCEPTED_CONFIG_FILE_FORMATS
                .split_last()
                .expect("there should be accepted file formats");
            let extensions = format!("{}, or {}", elements.join(", "), last);
            info!(
                "No tiles provided; assuming all files in the tile directory (except {} files)",
                extensions
            );
            // Set a display names for the horizontal and vertical fallback connectors
            // because process_connector() is not called on them.
            horizontal_connector_names.push("".to_string());
            vertical_connector_names.push("".to_string());

            let paths: Vec<_> = glob::glob(format!("{}/**/*", tile_directory).as_str())?
                .filter(|path| {
                    !ACCEPTED_CONFIG_FILE_FORMATS.contains(
                        &path
                            .as_ref()
                            .unwrap_or(&empty_path)
                            .extension()
                            .unwrap_or("".as_ref())
                            .to_str()
                            .unwrap_or(""),
                    )
                })
                .collect();

            let first_tile_path = match paths.first() {
                None => {
                    return Err(Error::ValueError(
                        "the list of tiles is empty, it must contain at least one element"
                            .to_string(),
                    ));
                },
                Some(path) => path.as_ref().unwrap_or(&empty_path),
            };

            let tile_type = TileType::determine_by_file_path(
                &PathBuf::from(tile_directory.clone()).join(first_tile_path),
            )?;
            let tile_constructor = Tile::determine_constructor_by_tile_type(&tile_type);

            info!(
                "No tile connectors provided; assuming they all have the same connector on all \
                 sides"
            );

            for entry in paths {
                if let Ok(entry) = entry {
                    if entry.is_dir() {
                        continue;
                    }
                    let name = entry.to_string_lossy().to_string();
                    let tile = tile_constructor(
                        tile_id,
                        name.clone(),
                        entry,
                        Connectors {
                            north: 2,
                            east: 2,
                            south: 2,
                            west: 2,
                        },
                        1.,
                    )?;
                    tiles.push(tile);

                    // Bump tile_id for the next iteration
                    tile_id += 1;
                }
            }

            tile_type
        };

        Ok(Self {
            tiles,
            horizontal_connector_names,
            vertical_connector_names,
            tile_type,
        })
    }

fn initialize_data_structures() -> (
        Vec<String>,
        Vec<String>,
        HashMap<Option<String>, usize>,
        HashMap<Option<String>, usize>,
        usize,
        usize,
        Vec<Tile>,
        usize,
    ) {
        // Mappings for determining the original connector name from a given
        // horizontal/vertical connector ID for error messaging purposes.
        // Set display names for the special wildcard and blank connector values 0 and
        // 1.
        let connector_names = vec!["🃟".to_string(), "None".to_string()];

        // This is used to map horizontal/vertical connector Strings from the config
        // file to usize connector IDs.
        // Map the blank connector, which is symbolized by None, Some("") and Some(" "),
        // to the ID 1.
        let connector_map = HashMap::from([(None, 1), (Some("".into()), 1), (Some(" ".into()), 1)]);

        // This is used to keep track of which horizontal (east and west) and vertical
        // (north and south) usize connector ID to assign to a connector String from the
        // config file that is not in connector_map yet. The final value plus one equals
        // horizontal_connector_count/vertical_connector_count. The value 0 is reserved
        // and represents the special wildcard connector. The value 1 is reserved and
        // represents the special blank connector. Consequently, IDs for regular
        // concrete connectors begin at 2.
        let current_connector_id = 1;

        let tiles = vec![];

        // Each tile has a tile_id that is equal to its index in tiles.
        let tile_id = 0;

        (
            connector_names.clone(),
            connector_names,
            connector_map.clone(),
            connector_map,
            current_connector_id,
            current_connector_id,
            tiles,
            tile_id,
        )
    }
}

/// Processing steps for all four connectors.
fn process_connector(
    connectors: &BTreeMap<String, &ValueKind>,
    side: &str,
    entry: &BTreeMap<String, &ValueKind>,
    name: &String,
    connector_map: &mut HashMap<Option<String>, usize>,
    connector_names: &mut Vec<String>,
    current_connector_id: &mut usize,
) -> Result<usize, Error> {
    let connector = if let Some(connector) = connectors.get(side) {
        connector.unpack_string_or_nil()?
    } else {
        return Err(Error::ValueError(format!(
            "the “connectors” section of the tile with the name “{name}” is missing the required \
             “{side}” connector:\n{entry:#?}"
        )));
    };

    Ok(if let Some(connector_id) = connector_map.get(&connector) {
        *connector_id
    } else {
        // The connector String is not present in connector_map.
        // Bump current_connector_id for every connector that isn’t found in
        // connector_map. The value `0` is reserved and represents the
        // special wildcard connector. Consequently, IDs for concrete
        // connectors begin at `1`.
        *current_connector_id += 1;
        // Add an entry for this connector.
        connector_map.insert(connector.clone(), *current_connector_id);

        // Add an entry for this connector in connector_names.
        connector_names.push(connector.unwrap_or("None".to_string()));

        *current_connector_id
    })
}
